These packages should be enough for our purposes.
Here are a couple plots that will serve as a basis for some examples, or can just give you a ready ggplot object to do your own exploration.
load('data/starwars_df.RData') # load starwars data frames
bar_sw = people_df %>%
mutate(homeworld = fct_lump(homeworld, n = 4)) %>%
drop_na(homeworld) %>%
ggplot() +
geom_bar(aes(x = homeworld, fill = homeworld), show.legend = F)
smooth_sw = people_df %>%
filter(!is.na(gender) & !grepl(name, pattern = 'Jabba')) %>%
ggplot(aes(x=mass, y=height)) +
geom_point() +
geom_smooth()
scatter_sw = starships_df %>%
arrange(length) %>%
ggplot(aes(x = length, y = cost_in_credits)) +
geom_point(aes(color = log(cost_in_credits*length)), size = 5, show.legend = F) +
geom_text_repel(aes(label = name), size=6, color = 'gray50') + # requires ggrepel
scale_x_continuous(trans = 'log') +
scale_y_continuous(trans = 'log') +
scale_color_viridis_c()
box_sw = people_df %>%
mutate(species = fct_lump(species, n = 2)) %>%
drop_na(species) %>%
ggplot(aes(x= species, y = mass)) +
geom_boxplot()continent_df = gapminder %>%
group_by(continent) %>%
summarise(lifeExp = mean(lifeExp),
gdpPercap = mean(gdpPercap))
country_df = gapminder %>%
group_by(continent, country) %>%
summarise(lifeExp = mean(lifeExp),
gdpPercap = mean(gdpPercap))
year_df = gapminder %>%
group_by(year) %>%
summarise(lifeExp = mean(lifeExp),
gdpPercap = mean(gdpPercap))
gapminder_std = gapminder %>%
group_by(year) %>%
mutate(lifeExp = scale(lifeExp)[,1],
gdpPercap = scale(gdpPercap)[,1])
bar_gap = gapminder %>%
group_by(continent) %>%
summarise(lifeExp = mean(lifeExp)) %>%
ggplot() +
geom_col(aes(x = continent, y = lifeExp, fill = continent), show.legend = F)
smooth_gap = gapminder %>%
ggplot(aes(x = gdpPercap, y = lifeExp)) +
geom_point() +
geom_smooth() +
scale_x_continuous(trans = 'log') +
scale_y_continuous(trans = 'log')
scatter_gap = country_df %>%
ggplot(aes(x = gdpPercap, y = lifeExp)) +
geom_point(aes(color = gdpPercap*lifeExp), show.legend = F) +
geom_text_repel(aes(label = country), size=2) +
scale_x_continuous(trans = 'log') +
scale_y_continuous(trans = 'log') +
scale_color_viridis_c()
box_gap = gapminder %>%
group_by(continent, country) %>%
summarise(lifeExp = mean(lifeExp)) %>%
ggplot() +
geom_boxplot(aes(x = continent, y = lifeExp, fill = continent), show.legend = F)We start with a simple example to get things going. Note that all animations will take at least several seconds to produce. To reduce this you can change the number of frames used, though this will typically make for a choppier plot. Note that output will be forced to the viewer instead of inline also.
box_sw +
transition_states(
species,
transition_length = 2,
state_length = 1
) +
enter_fade() +
exit_shrink() +
ease_aes('sine-in-out')To see what’s going on, let’s take it step by step. The transition_states function specifies how the plot information goes from one state to the next and how ‘states’ is defined. In this case, our states are represented by species. The transition_length is the ‘relative length of the transition’, but can be as many values as there are states. The state_length determines how long we pause (in terms of number of frames) at each state. We’ll talk about the others as we go along.
Typically you may want to assign the plot to an object so that you can use the animate function for more control.
There are many transitions, though _states and _reveal are probably ones you’ll use a lot. The former works on some categorical variable as we saw before, while the latter works on an ordered (numeric) variable. A variant of it is transition_time, which can work with an actual date/time variable. In the following, we examine life expectancy over time with the gapminder data. We’ll see how to pause the visualization at the end for a specific number of frames.
yr_life_exp = year_df %>%
ggplot(aes(x = year, y = lifeExp, group = 1)) +
geom_point(size = 10, color = '#ff5500') +
theme_minimal()
p_time = yr_life_exp +
transition_time(year)
animate(p_time, end_pause = 25)We can use the glue package to insert text into the titles. Each type of transition you create comes with named objects we can use. With transition_time, the unique value is frame_time, but others are generically available. See the corresponding help files for what all is available.
p_glue = yr_life_exp +
transition_time(year) +
ggtitle('Now showing {frame_time}',
subtitle = 'Frame {frame} of {nframes}')
animate(p_glue, nframes=50)Another that might come in handy is revealing the plot by the layers used to construct it. With the following we introduce some redundancy to take advantage of this.
mass_height = people_df %>%
filter(!is.na(gender) & !grepl(name, pattern = 'Jabba')) %>%
ggplot(aes(x=mass, y=height)) +
geom_point(alpha = .5) +
geom_point(aes(color = species == 'Human'), show.legend = F) +
geom_smooth(se = T, color = NA) +
geom_smooth(se = F, color = '#00aaff') +
geom_smooth(se = F, method = 'lm', color = '#ff5500') +
theme_void()
p = mass_height +
transition_layers()
animate(p, nframes = 50)The list of transitions includes:
transition_componentstransition_eventstransition_filtertransition_layerstransition_manualtransition_nulltransition_revealtransition_statestransition_timeShadows can add a nice effect to show paths over some index, especially time. The following recreates the famous TED talk visualization, which attempted to show how much better things were getting (if you ignored a lot of other factors).
p = gapminder %>%
ggplot(aes(x = gdpPercap, y = lifeExp)) +
geom_point(aes(color = country, size = pop), alpha = .25, show.legend = F) +
ggtitle('Year: {frame_time}') +
scale_size(guide = F) +
scale_x_continuous(trans = 'log') +
scale_y_continuous(trans = 'log', breaks = seq(30, 80, by = 10)) +
theme_minimal()
p +
transition_time(year) The following shows the data for just a handful of countries. We’ll use both shadow_mark and shadow_trail. The former can shadow past vs. future data points, while the latter has additional options for leaving a trail of symbols behind.
p = gapminder_std %>%
filter(country %in% c('China', 'India', 'Norway', 'United States')) %>%
ggplot(aes(x = gdpPercap, y = lifeExp)) +
geom_point(aes(color = country, size = pop), alpha = .25) +
ggtitle('Year: {frame_time}') +
scale_size(guide = F) +
theme_minimal()
p +
transition_time(year) +
shadow_mark()Maybe things hadn’t changed that much after all for some, relatively speaking, or only improved in one domain.
We have many options for how the plots enter and leave the space. In the following, we use enter_drift to let the previous plot linger a bit.
The following might be fun as an exercise to demonstrate grow/shrink and, but please don’t do this to try and spruce up a boring bar graph. After running, set the size of enter_grow to 1 to see what it changes.
Here are the options we can play with. Each enter_* function has a corresponding exit_* function.
enter_manual, exit_manualenter_appear, exit_disappearenter_fade, exit_fadeenter_grow, exit_shrinkenter_recolour, exit_recolourenter_recolor, exit_recolorenter_fly, exit_flyenter_drift, exit_driftenter_reset, exit_resetWith easing we can define the velocity with which aesthetics change during an animation. This is done with ease_aes. By default the transition is ‘linear’, but let’s see some others.
p = gapminder %>%
filter(year %in% c(1962, 2002)) %>%
mutate(year = factor(year)) %>%
ggplot(aes(x = gdpPercap, y = lifeExp)) +
geom_point(aes(color = country, group = 1), alpha = .25, show.legend = F)
p +
transition_states(year, transition_length = 5, state_length = 1) +
enter_fade() +
exit_fade() +
ease_aes(y = 'bounce-in-out')p +
transition_states(year, transition_length = 5, state_length = 1) +
enter_fade() +
exit_fade() +
ease_aes('elastic-in-out')Easing functions:
quadratic Models a power-of-2 functioncubic Models a power-of-3 functionquartic Models a power-of-4 functionquintic Models a power-of-5 functionsine Models a sine functioncircular Models a pi/2 circle arcexponential Models an exponential functionelastic Models an elastic release of energyback Models a pullback and releasebounce Models the bouncing of a ballModifiers
-in The easing function is applied as-is -out The easing function is applied in reverse -in-out The first half of the transition it is applied as-is, while in the last half it is reversed
When using animate, you have some options you’ll commonly want to alter that will affect the animation appearance. Here are the ones you’ll probably want to fiddle with most.
nframes The number of frames to render (default 100)fps The frame rate of the animation in frames/sec (default 10)duration The length of the animation in seconds (unset by default)start/end_pause Number of times to repeat the first and last frame in the animationrewind Should the animation roll back in the end (default FALSE)Often you’ll just get an error. This means you’ll need to read the documentation closely, as you likely have not used the correct variable type, and in general have not provided the function what it expects.
As an example, let’s try to use transition_reveal for a categorical variable.
Error: along data must either be integer, numeric, POSIXct, Date, difftime, orhms
It states clearly what’s needed for this transition, and we didn’t provide it. We can change to transition_states for example.
Sometimes what should work apparently does not. For example look at the following, which is our easing example from before. This time, no bounce!
p = people_df %>%
mutate(Human = factor(species == 'Human', labels = c('Other', 'Human'))) %>%
filter(!is.na(gender) & !grepl(name, pattern = 'Jabba')) %>%
drop_na(Human, mass, height) %>% # to remove warnings
ggplot(aes(x = mass, y = height)) +
geom_point(aes(color = Human), show.legend = T)
p +
transition_states(Human,
transition_length = 5,
state_length = 1) +
enter_fade() +
exit_fade() +
ease_aes(y = 'bounce-in-out')The variables Why does this happen? Because behind the scenes some sort of grouping is being done on the data. By specifying the color argument, we only have the two groups, while we want each individual point to bounce. If we add a group = 1 aesthetic for geom_point as we did the first time, we can get what we want. As stated in the documentation, the group aesthetic defines how the data in a layer is matched across the animation.
p = people_df %>%
mutate(Human = factor(species == 'Human', labels = c('Other', 'Human'))) %>%
filter(!is.na(gender) & !grepl(name, pattern = 'Jabba')) %>%
drop_na(Human, mass, height) %>% # to remove warnings
ggplot(aes(x = mass, y = height)) +
geom_point(aes(color = Human, group = 1), show.legend = T) # change made here
p +
transition_states(Human, transition_length = 5, state_length = 1) +
enter_fade() +
exit_fade() +
ease_aes(y = 'elastic-in-out')The previous plot makes it look as though humans are transitioning to/from non-humans. In this case, it would be more appropriate to not have such a transition ease. We can still use other effects, like enter/exit, if we like.
p = people_df %>%
mutate(Human = factor(species == 'Human', labels = c('Other', 'Human'))) %>%
filter(!is.na(gender) & !grepl(name, pattern = 'Jabba')) %>%
drop_na(Human, mass, height) %>% # to remove warnings
ggplot(aes(x = mass, y = height)) +
geom_point(aes(color = Human, group = Human), show.legend = T)
p +
transition_states(Human, transition_length = 2, state_length = 1) +
enter_fade() +
exit_drift(x_mod = 0, y_mod = 100)You may find a number of issues arise when using the animations in R Markdown, especially with non-default device settings, and even if it runs fine with the chunk code. It may be easiest to save the plot and load the file instead.
Animation is a great way to enhance a plot and tell a data driven story. It can definitely be overkill for many situations though, and so should be used with caution. With the right approach though, you can take your visualizations further, and have some fun doing it too!